package org.bhf.service;

import com.google.inject.Injector;
import com.google.inject.servlet.GuiceFilter;
import org.bhf.facilities.Project;
import org.bhf.facilities.ProjectInjectors;
import org.bhf.i18n.ContextLocation;

import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.net.HttpURLConnection;
import java.util.Locale;
import java.util.TimeZone;
import java.util.logging.Level;

/**
 * BHF Controller, implemented as a filter. Applications using the <code>Controller</code> MUST configure the default
 * Injector to include the bindings of the <code>ServiceModule</code>, which subclasses he Guice
 * <code>ServletModule</code>. Services are injector with a Project specific Injector, but the <code>Controller</code>,
 * <code>RequestManager</code>, and <code>ProcessView</code>s are injectored with the default Controller.
 */
public class Controller extends GuiceFilter
{
    /** The default location for the route specifgications is /WEB-INF/routes: UTF-8 encoded. */
    public static final String  ROUTE_SPECIFICATION_LOCATION        = "/WEB-INF/routes";

    /** To specify a non-default location */
    public static final String  ROUTE_SPECIFICATION_LOCATION_KEY    = "specifcation";

    /** Remember the servlet context */
    protected ServletContext    context;

    public void init( final FilterConfig filterConfig )
        throws ServletException
    {
        context = filterConfig.getServletContext();
    }

    public void doFilter( final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain )
        throws IOException, ServletException
    {
        super.doFilter( servletRequest, servletResponse, new FilterChain() {
            public void doFilter( ServletRequest servletRequest, ServletResponse servletResponse ) throws IOException, ServletException
            {
                innerFilter( servletRequest, servletResponse, filterChain );
            }
        });
    }

    private void innerFilter( final ServletRequest servletRequest, final ServletResponse servletResponse, final FilterChain filterChain )
        throws IOException, ServletException
    {
        final HttpServletRequest    request     = (HttpServletRequest)servletRequest;
        final HttpServletResponse   response    = (HttpServletResponse)servletResponse;
        final Injector              injector    = ProjectInjectors.getDefaultInjector();
        final URLRouter             router      = injector.getInstance( URLRouter.class );

        if( router == null )
            throw new ServletException( "You must provide a Routing Facility (org.bhf.service.URLRouter) implementation." );

        final URLRouter.RouteMatch route   = router.route( request.getRequestURI().substring( request.getContextPath().length() ), request.getServerName() );

        if( route != null && !route.isAntiRoute() && ServiceGroup.getServiceGroupByName( route.getServiceGroup() ) != null )
        {
            Project originalProject = null;

            try
            {
                originalProject = Project.setProject( new Project( route.getProject() ) );

                final RequestManager requestManager = injector.getInstance( RequestManager.class );

                requestManager.setArgJSON( emptyIsNull( request.getParameter( Constants.INVOCATION_ARGUMENT_PARAMETER ) ) );
                requestManager.setCsrToken( emptyIsNull( request.getParameter( Constants.TOKEN_PARAMETER ) ) );
                requestManager.setRoute( route );
                requestManager.setStateJSON( emptyIsNull( request.getParameter( Constants.OVERLAY_STATE_PARAMETER ) ) );
                requestManager.setStateVersion( emptyIsNullInteger( request.getParameter( Constants.COMPONENT_VERSION_PARAMETER ) ) );

                final boolean setContext = ContextLocation.getCurrentContextLocation() == null;
                if( setContext )
                {
                    final Locale locale = request.getLocale();

                    ContextLocation.pushContextLocation(
                        new ContextLocation( locale != null ? locale : Locale.getDefault(), TimeZone.getDefault() )
                    );
                }
                try
                {
                    final ProcessView view = requestManager.process();
                    response.setHeader( "Pragma", "No-cache" );
                    response.setHeader( "Cache-Control", "no-cache,no-store,max-age=0" );
                    response.setDateHeader( "Expires", 1 );
                    view.processView( request, response, context );
                }
                catch( final IllegalRequestException e )
                {
                    response.setStatus( HttpURLConnection.HTTP_BAD_REQUEST );
                    Package.log.log( Level.INFO, "Controller level exception", e );
                }
                catch( final RequestManager.UnknownPageException e )
                {
                    response.setStatus( HttpURLConnection.HTTP_NOT_FOUND );
                }
                catch( final Exception e )
                {
                    if( e instanceof ServletException ) throw (ServletException)e;
                    if( e instanceof RuntimeException ) throw (RuntimeException)e;
                    throw new ServletException( e );
                }
                finally
                {
                    if( setContext )
                    {
                        ContextLocation.popContextLocation();
                    }
                }
            }
            finally
            {
                Project.setProject( originalProject );
            }
        }
        else
        {
            filterChain.doFilter( servletRequest, servletResponse );
        }
    }

    public void destroy()
    {
    }

    //
    // Private
    //

    private static String   emptyIsNull( final String s )
    {
        return s == null || s.trim().length() == 0 ? null : s;
    }

    private static Integer  emptyIsNullInteger( String s )
    {
        s = emptyIsNull( s );
        return s != null ? Integer.parseInt( s ) : null;
    }
}
